package ClientsAndHotels;

import jade.core.AID;
import jade.core.behaviours.Behaviour;
import jade.core.behaviours.CyclicBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.lang.acl.UnreadableException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.Random;

import ClientsAndHotels.types.ClientParams;
import ClientsAndHotels.types.DiedAgentData;
import ClientsAndHotels.types.HotelRoom;
import ClientsAndHotels.types.OrderRequest;
import ClientsAndHotels.types.OrderResponse;
import ClientsAndHotels.types.Reservation;
import ClientsAndHotels.types.Room;
import jade.core.Agent;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ClientAgent extends CalendarObserverAgent {
    
    enum Action {
        
        SEARCH_MEDIATORS,
        SEND_QUERY_TO_MEDIATOR,
        RECEIVE_ANSWER_FROM_MEDIATOR,
        RECEIVE_ANSWER_FROM_HOTEL,
        CHOOSE_BEST_HOTEL,
        WAIT_FOR_BETTER,
        FINISHED,
        CANCEL_RESERVATION
    }
    // stan agenta
    private Action action = Action.SEARCH_MEDIATORS;
    // prawdopodobieństwo anulowania rezerwacji hotelu przez klienta
    public static final double REJECT_ROOM_PROB = .5;
    public static final String ACTION_GO_HOME = "go-home";
    // Informacje o zamówieniu
    private OrderRequest order;
    // pośrednik w wymianie informacjami
    private LinkedList<AID> mediatorAgents;
    // Generator liczb losowych
    private static Random rand = new Random();
    
    static {
        rand.setSeed(System.currentTimeMillis());
    }

    // Put agent initializations here
    @Override
    protected void setup() {
        
        notifyGuiAboutDie(DiedAgentData.TYPE_CLIENT);

        // Inicjalizacja
        Object[] args = getArguments();
        if (args.length == 1 && args[0] instanceof ClientParams) {
            ClientParams params = (ClientParams) args[0];
            order = new OrderRequest(params.getCity(),
                    params.getHotelStarsMin(), params.getHotelStarsMax(),
                    params.getRoomFor(), params.getMaxPrice(),
                    params.getDate(), params.getDays());
            // Wyświetl dodatkowe informacje na swój temat
            log("Przybył nowy klient " + getAID().getName()
                    + ". Poszukuje hotelu w mieście: " + order.getCity()
                    + ". Gwiazdki od " + order.getHotelStarsMin() + " do "
                    + order.getHotelStarsMax() + ". Dla " + order.getRoomFor()
                    + " osób. Max. cena: " + order.getMaxPrice() + ".");
            
            addBehaviour(new RequestPerformer());
            addBehaviour(new GoHomeBehaviour());
            super.setup();

        } else {
            // Zakończ swoje działanie
            log("Niewłaściwe parametry");
            doDelete();
        }
    }

    /**
     * Inner class RequestPerformer.
     */
    private class RequestPerformer extends Behaviour {

        // lista odpowiedzi od hoteli spełniających żądanie
        ArrayList<OrderResponse> response;
        // Schemat wiadomości odbieranych
        private MessageTemplate mt;
        // Wybrany pokój
        private HotelRoom theChosenHotelRoom;
        private boolean finished = false;
        private ACLMessage reserve;
        
        @Override
        public synchronized void action() {
            try {
                // Thread.sleep(1000);
                switch (action) {
                    case SEARCH_MEDIATORS:
                        findMediator(myAgent);
                        break;
                    case SEND_QUERY_TO_MEDIATOR:
                        sendQueryToMediator();
                        break;
                    case RECEIVE_ANSWER_FROM_MEDIATOR:
                        receiveAnswerFromMediator();
                        break;
                    case CHOOSE_BEST_HOTEL:
                        chooseBestHotel();
                        break;
                    case RECEIVE_ANSWER_FROM_HOTEL:
                        receiveAnswerFromHotel();
                        break;
                    case CANCEL_RESERVATION:
                        cancelReservation();
                        doDelete();
                        break;
                    //case WAIT_FOR_BETTER:
                        //waitForBetter();
                        //break;
                    case FINISHED:
                        finished = true;
                        break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
        private void findMediator(Agent myAgent) {
            log("Szukam hotelu dla siebie u pośrednika. "
                    + order.toString());
            // Znajdź pośrednika
            DFAgentDescription template = new DFAgentDescription();
            ServiceDescription sd = new ServiceDescription();
            sd.setType(MediatorAgent.TYPE);
            template.addServices(sd);
            try {
                DFAgentDescription[] result = DFService.search(myAgent, template);
                if (result.length <= 0) {
                    // log("Nie znaleziono pośredników.");
                } else {
                    // log("Znaleziono następujących pośredników:");
                    mediatorAgents = new LinkedList<>();
                    for (DFAgentDescription desc : result) {
                        mediatorAgents.add(desc.getName());
                        // log(desc.getName().getName());
                    }
                    if (!mediatorAgents.isEmpty()) {
                        action = Action.SEND_QUERY_TO_MEDIATOR;
                    }
                }
            } catch (FIPAException ex) {
                Logger.getLogger(ClientAgent.class.getName()).log(Level.SEVERE, null, ex);
            }
            
        }
        
        private void sendQueryToMediator() throws IOException {
            if (mediatorAgents.isEmpty()) {
                block(2000);
                action = Action.SEARCH_MEDIATORS;
                return;
            }
            
            // Wyślij zlecenie do jednego z pośredników
            ACLMessage cfp = new ACLMessage(ACLMessage.CFP);
            AID mediator = mediatorAgents.poll();
            log("Wysyłam moje zlecenie do mediatora " + mediator.getLocalName());
            cfp.addReceiver(mediator);
            cfp.setContentObject(order);
            cfp.setLanguage("JavaSerialization");
            cfp.setConversationId("hotel-search");
            cfp.setReplyWith("cfp" + System.currentTimeMillis());
            myAgent.send(cfp);
            // Przygotuj schemat wiadomości do odbioru informacji o
            // hotelach
            mt = MessageTemplate.and(MessageTemplate.and(MessageTemplate.and(
                    MessageTemplate.MatchConversationId("hotel-search"),
                    MessageTemplate.MatchInReplyTo(cfp.getReplyWith())),
                    MessageTemplate.MatchLanguage("JavaSerialization")),
                    MessageTemplate.MatchPerformative(ACLMessage.PROPOSE));
            action = Action.RECEIVE_ANSWER_FROM_MEDIATOR;
        }
        
        @SuppressWarnings("unchecked")
        private void receiveAnswerFromMediator() throws UnreadableException {
            // Odbierz informacje o hotelach od pośrednika
            ACLMessage reply = myAgent.receive(mt);
            if (reply != null) {
                response = (ArrayList<OrderResponse>) reply.getContentObject();
                // Obróbka otrzymanych danych
                log("Otrzymałem propozycje z " + response.size() + " hoteli.");
                if (response.isEmpty()) {
                    action = Action.SEARCH_MEDIATORS;
                } else {
                    action = Action.CHOOSE_BEST_HOTEL;
                }
            } else {
                block();
            }
        }
        
        private void chooseBestHotel() throws IOException {
            if (response != null && !response.isEmpty()) {
                ArrayList<HotelRoom> rooms = new ArrayList<>();
                for (OrderResponse hotel : response) {
                    for (Room room : hotel.getRooms()) {
                        rooms.add(new HotelRoom(hotel.getHotelId(), hotel.getStars(), room));
                    }
                }
                theChosenHotelRoom = getBestHotelFromList(rooms);
                if (theChosenHotelRoom != null) {
                    reserveRoom();
                } else {
                    log("Nie ma dla mnie pokoju.");
                    action = Action.SEND_QUERY_TO_MEDIATOR;
                }
            } else {
                theChosenHotelRoom = null;
                action = Action.SEND_QUERY_TO_MEDIATOR;
            }
        }
        
        private void reserveRoom() throws IOException {
            log("Rezerwuję pokój " + theChosenHotelRoom.getRoom().toString()
                    + " z hotelu "
                    + theChosenHotelRoom.getHotelId().getLocalName());
            reserve = new ACLMessage(ACLMessage.REQUEST);
            reserve.addReceiver(theChosenHotelRoom.getHotelId());
            reserve.setContentObject(new Reservation(theChosenHotelRoom.getRoom(), order.getDate(), order.getDays()));
            reserve.setLanguage("JavaSerialization");
            reserve.setConversationId("hotel-reserve");
            reserve.setReplyWith("reserve" + System.currentTimeMillis());
            action = Action.RECEIVE_ANSWER_FROM_HOTEL;
            myAgent.send(reserve);
        }
        
        private void receiveAnswerFromHotel() {
            // Przygotuj schemat wiadomości na odbiór odpowiedzi od
            // hotelu
            mt = MessageTemplate.and(
                    MessageTemplate.MatchConversationId("hotel-reserve"),
                    MessageTemplate.MatchInReplyTo(reserve.getReplyWith()));
            // Odbierz odpowiedź hotelu
            ACLMessage reply = myAgent.receive(mt);
            if (reply != null) {
                if (reply.getPerformative() == ACLMessage.AGREE) {
                    // Rejestracja została złożona
                    log("zarezerwowałem pokój "
                            + theChosenHotelRoom.getRoom().toString()
                            + " z hotelu "
                            + theChosenHotelRoom.getHotelId().getLocalName());

                    // Wyślij wiadomość do agenta GUI
                    ACLMessage message = new ACLMessage(ACLMessage.INFORM);
                    message.addReceiver(new AID("gui", false));
                    //message.setLanguage("JavaSerialization");
                    message.setConversationId("reservedRoom");
                    message.setContent(myAgent.getLocalName());
                    send(message);
                    
                    if (rand.nextDouble() <= REJECT_ROOM_PROB) {
                        log("wkrótce anuluję rezerwację");
                        action = Action.CANCEL_RESERVATION;
                        block(3000 + rand.nextInt(30) * 1000);
                    } else {
                        action = Action.FINISHED;
                    }
                } else if (reply.getPerformative() == ACLMessage.REFUSE) {
                    log("Niestety pokój zarezerwowany - spróbuję jeszcze raz.");
                    for (OrderResponse orderResponse : response) {
                        if (orderResponse.getHotelId() == theChosenHotelRoom.getHotelId()) {
                            orderResponse.removeRoom(theChosenHotelRoom.getRoom());
                            break;
                        }
                    }
                    theChosenHotelRoom = null;
                    action = Action.CHOOSE_BEST_HOTEL;
                } else {
                    log("Nieoczekiwana wiadomość.");
                    action = Action.RECEIVE_ANSWER_FROM_HOTEL;
                }
            } else {
                block();
            }
        }
        
        private void cancelReservation() throws IOException {
            log("Anuluję rezerwację na pokój "
                    + theChosenHotelRoom.getRoom().toString() + " z hotelu "
                    + theChosenHotelRoom.getHotelId().getLocalName());
            ACLMessage reserve = new ACLMessage(ACLMessage.REQUEST);
            reserve.addReceiver(theChosenHotelRoom.getHotelId());
            reserve.setLanguage("JavaSerialization");
            reserve.setConversationId(HotelAgent.ACTION_CANCEL_RESERVATION);
            reserve.setContentObject(new Reservation(theChosenHotelRoom.getRoom(), order.getDate(), order.getDays()));
            myAgent.send(reserve);
            theChosenHotelRoom = null;
        }
        
//        private void waitForBetter() throws UnreadableException, IOException {
//            // Przygotuj schemat wiadomości do odbioru od hotelu
//            mt = MessageTemplate.and(MessageTemplate.and(
//                    MessageTemplate.MatchConversationId("newRoomAvailable"),
//                    MessageTemplate.MatchLanguage("JavaSerialization")),
//                    MessageTemplate.MatchPerformative(ACLMessage.INFORM));
//
//            // Odbierz odpowiedź hotelu
//            ACLMessage reply = myAgent.receive(mt);
//            if (reply != null) {
//                log("Otrzymałem propozycję zmiany pokoju.");
//                ArrayList<HotelRoom> rooms = new ArrayList<>();
//                rooms.add(new HotelRoom(reply.getSender(), theChosenHotelRoom.getStars(), (Room) reply.getContentObject()));
//                rooms.add(theChosenHotelRoom);
//                HotelRoom currentBest = getBestHotelFromList(rooms);
//                if (currentBest.getRoom().getRoomId() != theChosenHotelRoom.getRoom().getRoomId()) {
//                    log("Zmieniam pokój, bo propozycja zmiany okazała się opłacalna!");
//                    cancelReservation();
//                    theChosenHotelRoom = currentBest;
//                    reserveRoom();
//                }
//            } else {
//                block();
//            }
//        }
        
        @Override
        public boolean done() {
            return finished;
        }
        
        private HotelRoom getBestHotelFromList(ArrayList<HotelRoom> rooms) {
            
            if (rooms.isEmpty()) {
                return null;
            }
            
            Collections.sort(rooms, new Comparator<HotelRoom>() {
                
                @Override
                public int compare(HotelRoom room1, HotelRoom room2) {
                    double price1 = room1.getRoom().getPrice();
                    double price2 = room2.getRoom().getPrice();
                    if (price1 == 0 && price2 == 0) {
                        return 0;
                    }
                    // jeśli różnica w cenach jest mniejsza niż 10%, to
                    // patrzymy na ilość gwiazdek; wpp. patrzymy na ceny
                    if (Math.abs(price1 - price2) / Math.max(price1, price2) < .1) {
                        return Integer.compare(-room1.getStars(),
                                -room2.getStars());
                    } else {
                        return Double.compare(price1, price2);
                    }
                }
            });
            return rooms.get(0);
        }
    }
    
    class GoHomeBehaviour extends CyclicBehaviour {
        
        @Override
        public void action() {
            MessageTemplate mt = MessageTemplate.and(
                    MessageTemplate.MatchPerformative(ACLMessage.INFORM),
                    MessageTemplate.MatchConversationId(ACTION_GO_HOME));
            ACLMessage msg = myAgent.receive(mt);
            if (msg != null) {
                doDelete();
            } else {
                block();
            }
        }
    }
    
    @Override
    protected synchronized void onDateChanged(Date date) {
        if (action != Action.FINISHED && action != Action.CANCEL_RESERVATION 
                && (order.getDate().before(date) || order.getDate().equals(date))) {
            log("Minął czas na rezerwację.");
            doDelete();
        }
    }
}
